package org.boz.dial;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Vector;


public class LispInterpreter {
	// private LispObject top_env;
	private abstract class LispOperator extends LispObject {
		public LispObject eval(LispObject env) {
			return this;
		}
	}

	private class LispPlus extends LispOperator {
		public String toString() {
			return "+"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			int result = ((LispInt) vals.car()).value();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				result += ((LispInt) val.car()).value();
			}
			return new LispInt(result);
		}
	}

	private class LispMinus extends LispOperator {
		public String toString() {
			return "-"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			int result = ((LispInt) vals.car()).value();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				result -= ((LispInt) val.car()).value();
			}
			return new LispInt(result);
		}
	}

	private class LispMultiply extends LispOperator {
		public String toString() {
			return "*"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			int result = ((LispInt) vals.car()).value();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				result *= ((LispInt) val.car()).value();
			}
			return new LispInt(result);
		}
	}

	private class LispDivide extends LispOperator {
		public String toString() {
			return "/"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			int result = ((LispInt) vals.car()).value();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				result /= ((LispInt) val.car()).value();
			}
			return new LispInt(result);
		}
	}

	private class LispEqual extends LispOperator {
		public String toString() {
			return "="; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			LispObject o1 = vals.car();
			LispObject o2 = vals.cdr().car();
			if (((LispInt) o1).value() == ((LispInt) o2).value())
				return LispPrimitives.tee;
			return LispPrimitives.nil;
		}
	}

	private class LispGreater extends LispOperator {
		public String toString() {
			return ">"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			LispObject o1 = vals.car();
			LispObject o2 = vals.cdr().car();
			if (((LispInt) o1).value() > ((LispInt) o2).value())
				return LispPrimitives.tee;
			return LispPrimitives.nil;
		}
	}

	private class LispLess extends LispOperator {
		public String toString() {
			return "<"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			LispObject o1 = vals.car();
			LispObject o2 = vals.cdr().car();
			if (((LispInt) o1).value() < ((LispInt) o2).value())
				return LispPrimitives.tee;
			return LispPrimitives.nil;
		}
	}

	private class LispCarOp extends LispOperator {
		public String toString() {
			return "car"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			return vals.car().car();
		}
	}

	private class LispCdrOp extends LispOperator {
		public String toString() {
			return "cdr"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			return vals.car().cdr();
		}
	}

	private class LispConsOp extends LispOperator {
		public String toString() {
			return "cons"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			return new LispCons(vals.car(), vals.cdr().car());
		}
	}

	private class LispConcat extends LispOperator {
		public String toString() {
			return "concat"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			String result = ((LispString) vals.car()).value();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				result += ((LispString) val.car()).value();
			}
			return new LispString(result);
		}
	}

	private class LispStringEqual extends LispOperator {
		public String toString() {
			return "string-equals"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			String s1 = ((LispString) vals.car()).value();
			String s2 = ((LispString) vals.cdr().car()).value();
			if (s1.equals(s2))
				return LispPrimitives.tee;
			return LispPrimitives.nil;
		}
	}

	private class LispNew extends LispOperator {
		public String toString() {
			return ":new"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			String className = ((LispString) vals.car()).value();
			// FIXME: Add support for parameter list
			Vector<Object> args = new Vector<Object>();
			for (LispObject val = vals.cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				args.add(val.car().toObject());
			}
			Class<?>[] argTypes = new Class[args.size()];
			for (int i = 0; i < args.size(); ++i) {
				argTypes[i] = args.get(0).getClass();
			}

			Class<?> cls;
			try {
				cls = Class.forName(className);
			} catch (Throwable e) {
				throw new RuntimeException("Couldn't find a class named " //$NON-NLS-1$
						+ className);
			}

			try {
				Constructor<?> ct = cls.getConstructor(argTypes);
				Object result = ct.newInstance(args.toArray());
				return toLisp(result);
			} catch (Throwable e) {
			}

			// Try the trickier method
			Constructor<?>[] cts = cls.getConstructors();
			for (int i = 0; i < cts.length; ++i) {
				Constructor<?> ct = cts[i];
				try {
					Object result = ct.newInstance(args.toArray());
					return toLisp(result);
				} catch (Throwable e2) {
				}
			}
			throw new RuntimeException("Failed to create object. Class " //$NON-NLS-1$
					+ className
					+ " has no constructor matching the argument types given"); //$NON-NLS-1$
		}
	}

	private class LispInvoke extends LispOperator {
		public String toString() {
			return ":invoke"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			String errors = new String();
			Object o = vals.car().toObject();
			Class<?> cls = o.getClass();
			String methodName = ((LispString) vals.cdr().car()).value();
			Vector<Object> args = new Vector<Object>();
			for (LispObject val = vals.cdr().cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				args.add(val.car().toObject());
			}
			String typesString = new String();
			Class<?>[] argTypes = new Class[args.size()];
			for (int i = 0; i < args.size(); ++i) {
				argTypes[i] = args.get(i).getClass();
				if(typesString.length() > 0)
					typesString += ", "; //$NON-NLS-1$
				typesString += argTypes[i].getCanonicalName();
			}

			// Try the simple method first, where all params just match
			try {
				Method method = cls.getMethod(methodName, argTypes);
				Object result = method.invoke(o, args.toArray());
				return toLisp(result);
			} catch(InvocationTargetException t) {
				throw new RuntimeException(":invoke failed, method " + methodName //$NON-NLS-1$
					+ " on object " + o //$NON-NLS-1$
					+ " of type: " + cls //$NON-NLS-1$
					+ " has thrown an exception: " + t.getCause()); //$NON-NLS-1$
			} catch (Throwable e) {
			}

			// Try the trickier method
			String possibleMethods = new String();
			Method[] methods = cls.getMethods();
			for (int i = 0; i < methods.length; ++i) {
				Method m = methods[i];
				if (m.getName().equals(methodName)) {
					if(possibleMethods.length() > 0)
						possibleMethods += ", "; //$NON-NLS-1$
					possibleMethods += m;
					try {
						Object result = m.invoke(o, args.toArray());
						return toLisp(result);
					} catch (InvocationTargetException t2) {
						throw new RuntimeException(":invoke failed, method " + methodName //$NON-NLS-1$
								+ " on object " + o //$NON-NLS-1$
								+ " of type: " + cls  //$NON-NLS-1$
								+ " has thrown an exception: " + t2.getCause()); //$NON-NLS-1$
					} catch (Throwable e2) {
					}
				}
			}
			throw new RuntimeException(":invoke failed, object " + o //$NON-NLS-1$
					+ " of type: " + cls //$NON-NLS-1$
					+ " has no method named " + methodName //$NON-NLS-1$
					+ " or matching the argument types given: " + typesString //$NON-NLS-1$
					+ ".\nCandidates are: " + possibleMethods //$NON-NLS-1$
					+ ".\n " + errors); //$NON-NLS-1$
		}
	}

	private class LispInvokeStatic extends LispOperator {
		public String toString() {
			return ":invoke-static"; //$NON-NLS-1$
		}

		public LispObject apply(LispObject vals, LispObject env) {
			final String className = ((LispString) vals.car()).value();
			Class<?> cls;
			try {
				cls = Class.forName(className);
			} catch (Throwable e) {
				throw new RuntimeException("Couldn't find class " + className); //$NON-NLS-1$
			}
			final String methodName = ((LispString) vals.cdr().car()).value();
			Vector<Object> args = new Vector<Object>();
			for (LispObject val = vals.cdr().cdr(); val != LispPrimitives.nil; val = val
					.cdr()) {
				args.add(val.car().toObject());
			}
			Class<?>[] argTypes = new Class[args.size()];
			for (int i = 0; i < args.size(); ++i) {
				argTypes[i] = args.get(i).getClass();
			}

			// Try the simple method first, where all params just match
			try {
				Method method = cls.getMethod(methodName, argTypes);
				Object result = method.invoke(null, args.toArray());
				return toLisp(result);
			} catch (Throwable e) {
			}

			// Try the trickier method
			Method[] methods = cls.getMethods();
			for (int i = 0; i < methods.length; ++i) {
				Method m = methods[i];
				if (m.getName().equals(methodName)) {
					try {
						Object result = m.invoke(null, args.toArray());
						return toLisp(result);
					} catch (Throwable e2) {
					}
				}
			}
			throw new RuntimeException(":invoke-static failed, class " //$NON-NLS-1$
					+ className + " has no method named " + methodName //$NON-NLS-1$
					+ " or matching the argument types given"); //$NON-NLS-1$
		}
	}

	private LispObject all_symbols;

	private LispObject findsym(String name) {
		LispObject symlist;
		for (symlist = all_symbols; symlist != LispPrimitives.nil; symlist = symlist
				.cdr()) {
			if (((LispSym) (symlist.car())).name().equals(name))
				return symlist.car();
		}
		return LispPrimitives.nil;
	}

	private LispObject intern(String name) {
		name = name.toUpperCase();
		LispObject op = findsym(name);
		if (op != LispPrimitives.nil)
			return op;
		op = new LispSym(name);
		all_symbols = new LispCons(op, all_symbols);
		return op;
	}

	// FIXME: Should be private, non-static, but top_env isn't a member (yet)
	public static LispObject extend_top(LispObject sym, LispObject val) {
		// LispPrimitives.top_env = new LispCons(new LispCons(sym,val),
		// LispPrimitives.top_env);
		((LispCons) LispPrimitives.top_env).setcdr(new LispCons(new LispCons(
				sym, val), LispPrimitives.top_env.cdr()));
		return val;
	}

	private LispObject toLisp(Object o) {
		Class<?> cls = o.getClass();
		if (cls.equals(String.class))
			return new LispString((String) o);
		if (cls.equals(Integer.class))
			return new LispInt(((Integer) o).intValue());
		if (cls.equals(Boolean.class))
			return (((Boolean) o).booleanValue()) ? LispPrimitives.tee
					: LispPrimitives.nil;
		return new LispJavaObject(o);
	}

	private Vector<String> get_tokens(String rest) {
		Vector<String> tokens = new Vector<String>();
		while (rest.length() > 0) {
			rest = rest.trim();
			if (rest.charAt(0) == '(') {
				tokens.addElement("("); //$NON-NLS-1$
				rest = rest.substring(1);
			} else if (rest.charAt(0) == ')') {
				tokens.addElement(")"); //$NON-NLS-1$
				rest = rest.substring(1);
			} else if (rest.charAt(0) == '\'') {
				tokens.addElement("'"); //$NON-NLS-1$
				rest = rest.substring(1);
			} else if (rest.charAt(0) == '"') {
				int length = rest.indexOf('"', 1) + 1;
				if (length < 0) {
					throw new RuntimeException("Unterminated string constant"); //$NON-NLS-1$
				}
				tokens.addElement(rest.substring(0, length));
				rest = rest.substring(length);
			} else { // we must have another token
				int length = 0;
				for (length = 0; length < rest.length(); ++length) {
					if ((rest.charAt(length) == '(')
							|| (rest.charAt(length) == '\'')
							|| (rest.charAt(length) == ')')
							|| (rest.charAt(length) == ' '))
						break;
				}
				tokens.addElement(rest.substring(0, length));
				rest = rest.substring(length);
			}
		}
		return tokens;
	}

	private LispObject readList(Vector<String> tokens) {
		if (tokens.size() < 1)
			throw new RuntimeException("Syntax error"); //$NON-NLS-1$
		String token = (String) tokens.remove(0);
		if (token.equals(")")) //$NON-NLS-1$
			return LispPrimitives.nil;
		tokens.insertElementAt(token, 0); // put it back!
		LispObject o = readObject(tokens);
		return new LispCons(o, readList(tokens));
	}

	private LispObject readObject(Vector<String> tokens) {
		if (tokens.size() < 1)
			throw new RuntimeException("Syntax error"); //$NON-NLS-1$
		String token = (String) tokens.remove(0);
		if (token.equals("(")) //$NON-NLS-1$
			return readList(tokens);
		if (token.equals("'")) //$NON-NLS-1$
			return new LispCons(LispPrimitives.LispQuote, new LispCons(
					readObject(tokens), LispPrimitives.nil));
		if (token.charAt(0) == '"')
			return new LispString(token.substring(1, token.length() - 1));

		try {
			Integer i = new Integer(token);
			return new LispInt(i.intValue());
		} catch (Exception e) {
		}
		return intern(token);
	}

	public LispObject[] parse(String input) {
		Vector<String> tokens = get_tokens(input);
		Vector<LispObject> objects = new Vector<LispObject>();
		while (tokens.size() > 0) {
			LispObject o = readObject(tokens);
			objects.add(o);
		}
		return objects.toArray(new LispObject[] {});
	}

	public void addSymbol(String sym, Object val) {
		extend_top(intern(sym), toLisp(val));
	}

	public LispObject parseEval(String input) {
		LispObject[] objects = parse(input);
		LispObject result = LispPrimitives.nil;
		for (int i = 0; i < objects.length; ++i) {
			result = eval(objects[i]);
		}
		return result;
	}

	public LispInterpreter() {
		LispPrimitives.nil = new LispSym("NIL"); //$NON-NLS-1$

		LispPrimitives.top_env = new LispCons(new LispCons(LispPrimitives.nil,
				LispPrimitives.nil), LispPrimitives.nil);
		all_symbols = new LispCons(LispPrimitives.nil, LispPrimitives.nil);

		LispPrimitives.tee = intern("t"); //$NON-NLS-1$
		extend_top(LispPrimitives.tee, LispPrimitives.tee);
		LispPrimitives.LispIf = intern("if"); //$NON-NLS-1$
		LispPrimitives.LispLambda = intern("lambda"); //$NON-NLS-1$
		LispPrimitives.LispQuote = intern("quote"); //$NON-NLS-1$
		LispPrimitives.LispSet = intern("set"); //$NON-NLS-1$
		LispPrimitives.LispDefine = intern("define"); //$NON-NLS-1$
		LispPrimitives.LispLet = intern("let"); //$NON-NLS-1$
		LispPrimitives.LispProgn = intern("progn"); //$NON-NLS-1$
		LispPrimitives.LispDefMacro = intern("defmacro"); //$NON-NLS-1$

		extend_top(intern("+"), new LispPlus()); //$NON-NLS-1$
		extend_top(intern("-"), new LispMinus()); //$NON-NLS-1$
		extend_top(intern("*"), new LispMultiply()); //$NON-NLS-1$
		extend_top(intern("/"), new LispDivide()); //$NON-NLS-1$

		extend_top(intern("="), new LispEqual()); //$NON-NLS-1$
		extend_top(intern(">"), new LispGreater()); //$NON-NLS-1$
		extend_top(intern("<"), new LispLess()); //$NON-NLS-1$

		extend_top(intern("car"), new LispCarOp()); //$NON-NLS-1$
		extend_top(intern("cdr"), new LispCdrOp()); //$NON-NLS-1$
		extend_top(intern("cons"), new LispConsOp()); //$NON-NLS-1$

		extend_top(intern("concat"), new LispConcat()); //$NON-NLS-1$
		extend_top(intern("string-equal"), new LispStringEqual()); //$NON-NLS-1$

		extend_top(intern(":new"), new LispNew()); //$NON-NLS-1$
		extend_top(intern(":invoke"), new LispInvoke()); //$NON-NLS-1$
		extend_top(intern(":invoke-static"), new LispInvokeStatic()); //$NON-NLS-1$

		parseEval(Startup.getString("Startup.0"));
		// System.out.println("all_symbols: " + all_symbols);
		// System.out.println("top_env: " + LispPrimitives.top_env);
	}

	public LispObject eval(LispObject o) {
		return o.eval(LispPrimitives.top_env);
	}
}
